<?php
// +----------------------------------------------------------------------
// | INPHP
// | Copyright (c) 2023 https://inphp.cc All rights reserved.
// | Licensed ( https://opensource.org/licenses/MIT )
// | Author: 幺月儿(https://gitee.com/lulanyin) Email: inphp@qq.com
// +----------------------------------------------------------------------
// | HTTP服务对象，包含WS(如果启动)
// +----------------------------------------------------------------------
namespace Inphp\Core\Services\Http;

use Inphp\Core\Config;
use Inphp\Core\Context;
use Inphp\Core\Middlewares;
use Inphp\Core\Object\Client;
use Inphp\Core\Object\Message;
use Inphp\Core\Router;
use Inphp\Core\Service;
use Inphp\Core\Services\IServer;
use Inphp\Core\Util\File;
use Inphp\Core\Util\Str;
use Swoole\Coroutine;
use Swoole\Process;

class Server implements IServer
{
    /**
     * 服务绑定的IP
     * @var string
     */
    public string $ip = "0.0.0.0";

    /**
     * 端口
     * @var int
     */
    public int $port = 1990;

    /**
     * 主服务对象
     * 支持http和websocket
     * @var \Swoole\Http\Server|\Swoole\WebSocket\Server|null
     */
    public \Swoole\Http\Server|\Swoole\WebSocket\Server|null $server = null;

    /**
     * 服务端类型
     * @var string
     */
    public string $type = "http";

    /**
     * 服务端配置
     * @var array
     */
    public array $settings = [
        //PID文件保存位置，文件夹必须存在
        "pid_file"              => __DIR__."/../../runtime/service.pid",
        //worker 数量，一般按CPU核心数量 * 2
        "worker_num"            => 2,
        //最大请求数量，按需，不可超过系统设置
        "max_request"           => 128,
        //最大连接数量，按需，不可超过系统配置
        "max_connection"        => 256,
        //日志文件，文件夹必须存在
        "log_file"              => __DIR__."/../../runtime/service.txt",
        //默认异步进程数量
        "task_worker_num"       => 2,
        //数据包大小
        "package_max_length"    => 8092,
        //文件上传临时保存目录
        "upload_tmp_dir"        => __DIR__."/../../runtime/upload",
        //是否开启静态文件，静态文件处理，交给nginx代理吧
        "enable_static_handler" => false,
        //默认静态文件目录，文件夹必须存在，一般使用nginx代理完成静态文件访问
        "document_root"         => __DIR__."/../../runtime/public",
        //开启post解析，默认是开启的
        "http_parse_post"       => true,
        //开启cookie解析，默认是开启的
        "http_parse_cookie"     => true,
        //开启文件上传解析，默认是开启的
        "http_parse_files"      => true,
        //启用压缩，默认是开启的
        "http_compression"      => true,
        //压缩级别，1~9，级别越高，消耗CPU越多，尺寸越小
        "http_compression_level"=> 1
    ];

    /**
     * 热更新进程
     * @var Process|null
     */
    private Process|null $hotUpdateProcess = null;

    /**
     * 初始化
     * @param bool $cli     是否属于CLI运行，如果使用CLI运行，则会启动swoole的服务端
     * @param string $type  启动的服务端类型(这里使用字符串，主要为了未来可升级为更多的服务端类型)
     */
    public function __construct(bool $cli = false, string $type = "http")
    {
        //非CLI运行时，仅支持HTTP
        $this->type = $cli ? $type : "http";
        //获取配置
        $config = Config::get("server", []);
        $this->ip = $config["ip"] ?? $this->ip;
        $this->port = $config["port"] ?? $this->port;
        //如果是基于CLI运行
        if ($cli) {
            //某些配置需要合并
            $this->settings = array_merge($this->settings, $config["http"] ?? []);
            if ($type == Service::WEBSOCKET) {
                $this->settings = array_merge($this->settings, $config["ws"] ?? ($config["websocket"] ?? []));
            }
            //数据包分发策略使用固定模式，根据连接的文件描述符分配 Worker。这样可以保证同一个连接发来的数据只会被同一个 Worker 处理
            //参数说明：https://wiki.swoole.com/#/server/setting?id=dispatch_mode
            $this->settings["dispatch_mode"] = 2;
            //Hook
            Coroutine::set(["hook_flags" => SWOOLE_HOOK_ALL]);
            //
            if ($type == Service::WEBSOCKET) {
                $this->server = new \Swoole\WebSocket\Server($this->ip, $this->port, $config["mode"] ?? 2, $config["sockType"] ?? 1);
            } else {
                $this->server = new \Swoole\Http\Server($this->ip, $this->port, $config["mode"] ?? 2, $config["sockType"] ?? 1);
            }
            $this->server->set($this->settings);
            //server的各个事件
            $this->server->on("WorkerStart", [$this, "onWorkerStart"]);
            $this->server->on("start", [$this, "onStart"]);
            $this->server->on("task", [$this, "onTask"]);
            $this->server->on("finish", [$this, "onFinish"]);
            //http
            $this->server->on("request", [$this, "onRequest"]);
            //ws
            if ($this->type === Service::WEBSOCKET) {
                $this->server->on("connect", [$this, "onConnect"]);
                $this->server->on("open", [$this, "onOpen"]);
                $this->server->on("message", [$this, "onMessage"]);
                $this->server->on("close", [$this, "onClose"]);
            }
            //热更新
            $hotUpdate = $config["hotUpdate"] ?? [];
            $hotUpdateEnable = (defined("INPHP_HOT_UPDATE") && INPHP_HOT_UPDATE) || ($hotUpdate["enable"] ?? false);
            if ($hotUpdateEnable) {
                $this->hotUpdateProcess = new Process([$this, "hotUpdate"], false, 2, true);
                $this->server->addProcess($this->hotUpdateProcess);
            }
        }
    }

    /**
     * 主进程开始启动
     */
    public function onStart()
    {
        $ip = $this->ip;
        echo "[{$this->type}]服务已启动，地址是：".($this->type === Service::WEBSOCKET ? "ws" : "http")."://{$ip}:{$this->port}".PHP_EOL;
        //热更新
        /*
        $config = Config::get("server.hotUpdate");
        $hotUpdateEnable = (defined("INPHP_HOT_UPDATE") && INPHP_HOT_UPDATE) || $config["enable"];
        if ($hotUpdateEnable) {
            $seconds = $config["seconds"] ?? 5;
            $seconds = is_numeric($seconds) && $seconds > 0 && $seconds <= 60 ? $seconds : 5;
            Timer::tick($seconds * 1000, function () {
                Coroutine::create(function () {
                    $this->hotUpdate();
                });
            });
        }*/
        //处理中间件
        $this->processMiddleware("onStart", func_get_args());
    }

    /**
     * 子进程启动
     */
    public function onWorkerStart()
    {
        $this->processMiddleware("onWorkerStart", func_get_args());
    }

    /**
     * 异步任务投递事件
     */
    public function onTask()
    {
        $this->processMiddleware("onTask", func_get_args());
    }

    /**
     * 异步任务完成
     */
    public function onFinish()
    {
        $this->processMiddleware("onFinish", func_get_args());
    }

    /**
     * HTTP GET/POST/OPTIONS... 请求
     */
    public function onRequest(\Swoole\Http\Request|null $swooleHttpRequest = null, \Swoole\Http\Response|null $swooleHttpResponse = null)
    {
        //被禁止的请求方式
        $disableHttpRequestMethod = Config::get("server.disableHttpRequestMethod", []);
        if (Service::isCLI()) {
            //CLI运行的swoole server
            //IP，需要Nginx代理添加字段
            $ip = $swooleHttpRequest->header["x-real-ip"] ?? ($swooleHttpRequest->server["remote_addr"] ?? null);
            //路径信息
            $path = $swooleHttpRequest->server["path_info"] ?? "";
            $uri = $swooleHttpRequest->server["request_uri"] ?? "";
            $path = !empty($path) ? $path : $uri;
            //chrome的多余请求，直接在处理掉
            if (strrchr($path, "favicon.ico") === "favicon.ico") {
                $swooleHttpResponse->end();
                return;
            }
            if (in_array(strtolower($swooleHttpRequest->getMethod()), $disableHttpRequestMethod)) {
                $swooleHttpResponse->status(403);
                $swooleHttpResponse->end();
                return;
            }
            // header 的 key 全部大写
            $headers = [];
            if (!empty($swooleHttpRequest->header)) {
                foreach ($swooleHttpRequest->header as $key => $val) {
                    $name = Str::trim($key);
                    $name = strtoupper($name);
                    $name = str_replace(" ", "_", $name);
                    $name = str_replace("-", "_", $name);
                    $headers[$name] = $val;
                }
            }
            //客户端对象(由于swoole运行的服务端，不支持session对象，需要另外实现)
            $client = new Client([
                "https"     => ($swooleHttpRequest->header["x-request-scheme"] ?? null) == "https",
                "host"      => $swooleHttpRequest->header["host"],
                "ip"        => $ip,
                "server"    => $swooleHttpRequest->server,
                "method"    => $swooleHttpRequest->getMethod(),
                "cookie"    => $swooleHttpRequest->cookie,
                "get"       => $swooleHttpRequest->get,
                "post"      => $swooleHttpRequest->post,
                "files"     => $swooleHttpRequest->files,
                "rawData"   => $swooleHttpRequest->rawContent(),
                "uri"       => $path,
                "id"        => $swooleHttpRequest->fd,
                "origin"    => $swooleHttpRequest->header["origin"] ?? "",
                "header"    => $headers,
                "contentType" => $swooleHttpRequest->server["content-type"] ?? ""
            ]);
        } else {
            if (in_array(strtolower($_SERVER["REQUEST_METHOD"]), $disableHttpRequestMethod)) {
                header("Content-Type: charset=utf-8;", false, 403);
                return;
            }
            //路径信息
            $path = $_SERVER["PATH_INFO"] ?? "";
            $uri = $_SERVER["REQUEST_URI"] ?? "";
            $path = !empty($path) ? $path : $uri;
            //chrome的多余请求，直接在处理掉
            if (strrchr($path, "favicon.ico") === "favicon.ico") {
                return;
            }
            //客户端
            $client = new Client([
                "https"     => (isset($_SERVER["REQUEST_SCHEME"]) && $_SERVER["REQUEST_SCHEME"] == "https") || (isset($_SERVER["HTTP_X_FORWARDED_PROTO"]) && $_SERVER["HTTP_X_FORWARDED_PROTO"] === "https"),
                "host"      => $_SERVER["HTTP_HOST"],
                "ip"        => $_SERVER["REMOTE_ADDR"] ?? "127.0.0.1",
                "server"    => $_SERVER,
                "method"    => $_SERVER["REQUEST_METHOD"],
                "cookie"    => $_COOKIE,
                "get"       => $_GET,
                "post"      => $_POST,
                "files"     => $_FILES,
                "rawData"   => file_get_contents("php://input") ?? null,
                "uri"       => $path,
                "id"        => 0,
                "origin"    => $_SERVER["HTTP_ORIGIN"] ?? "",
                "contentType" => $_SERVER["CONTENT_TYPE"] ?? ""
            ]);
            //设置header
            $headers = [];
            foreach ($_SERVER as $key => $val) {
                if (stripos($key, "HTTP_") === 0) {
                    $name = Str::trim($key);
                    $name = strtoupper(substr($name, 5));
                    $name = str_replace(" ", "_", $name);
                    $name = str_replace("-", "_", $name);
                    $headers[$name] = $val;
                }
            }
            $client->header = $headers;
        }
        //保存客户端对象到临时上下文
        Context::setClient($client);
        //初始化session
        Session::init();
        //创建http response对象
        $response = new Response();
        $response->setServer($this);
        if (Service::isCLI()) {
            //在CLI运行时
            $response->setSwooleHttpRequest($swooleHttpRequest)
                ->setSwooleHttpResponse($swooleHttpResponse);
        }
        //保存到上下文
        Context::setResponse($response);
        try {
            //中间件处理
            $this->processMiddleware("onRequest", [$response]);
            if ($response->end) {
                return;
            }
            //路由处理
            $status = Router::process($client->host, $client->uri, $client->method, Service::HTTP);
            //得到状态后，处理路由状态，并响应数据
            $response->setRouterStatus($status)->ending();
        } catch (\Exception $exception) {
            //抛出异常有中间件处理
            $this->processMiddleware("onRequestException", [$response, $exception]);
            if (!$response->end) {
                //默认响应 500 错误
                $response->error(500, $exception->getMessage());
            }
        }
        //清除临时上下文
        Context::delete();
    }

    /**
     * 新的ws客户端连接，HTTP不支持该事件
     */
    public function onConnect(\Swoole\WebSocket\Server $server, int $fd, int $reactorId): void
    {
        //保存websocket server到当前协程的上下文
        Context::setServer($server);
        //告诉客户端连接成功

        $this->processMiddleware("onConnect", func_get_args());
    }

    /**
     * WS 新客户端连接
     * @param \Swoole\WebSocket\Server $server
     * @param \Swoole\Http\Request $request
     */
    public function onOpen(\Swoole\WebSocket\Server $server, \Swoole\Http\Request $swooleHttpRequest): void
    {
        //主域名
        $host = $swooleHttpRequest->header['host'];
        $ip = $swooleHttpRequest->header['x-real-ip'] ?? ($swooleHttpRequest->server['remote_addr'] ?? null);
        //保存客户端信息，相对http请示，ws的客户端信息会少一些
        $client = new Client([
            "https"     => ($swooleHttpRequest->header["X-Request-Scheme"] ?? null) == "https",
            "host"      => $host,
            "ip"        => $ip,
            "method"    => 'upgrade',
            "header"    => $swooleHttpRequest->header,
            "cookie"    => $swooleHttpRequest->cookie,
            "get"       => $swooleHttpRequest->get,
            "origin"    => $swooleHttpRequest->header['origin'] ?? '',
            "id"        => $swooleHttpRequest->fd,
            "workerId"  => $server->worker_id
        ], true);
        //保存到当前协程上下文
        Context::setClient($client);
        //保存websocket server到当前协程的上下文
        Context::setServer($server);
        //通知客户端的fd
        $server->push($swooleHttpRequest->fd, (new Message())->event("opened")->data(["id" => $client->id, "workerId" => $server->worker_id])->toJson());
        //执行中间件
        $this->processMiddleware("onOpen", func_get_args());
    }

    /**
     * WS 收到客户端消息，处理消息即可，无需响应
     */
    public function onMessage(\Swoole\WebSocket\Server $server, \Swoole\WebSocket\Frame $frame): void
    {
        //获取客户端数据
        if (!($client = Client::fromCache($server->worker_id, $frame->fd))) {
            $this->processMiddleware("onUnknownClient", func_get_args());
            return;
        }
        //保存到临时上下文，方便控制器使用
        Context::setClient($client);
        //保存websocket server到当前协程的上下文
        Context::setServer($server);
        //仅接收JSON数据，必须保证数据格式
        $data = !empty($frame->data) ? (@json_decode($frame->data, true) ?? null) : null;
        if (!empty($data)) {
            $uri = $data["path"] ?? ($data["uri"] ?? ($data["event"] ?? ""));
            //记录
            $client->set("uri", $uri);
            //记录最后消息
            $client->setOtherData("lastMessage", $data);
            $client->setOtherData("lastMessageTime", time());
            //保存一下临时上下文需要用到的对象
            Context::setServer($server);
            //如果配置了 onMessage 中间件，会直接接管，不再进行处理
            $onMessageMiddleware = Middlewares::get("onMessage");
            if (!empty($onMessageMiddleware)) {
                $this->processMiddleware("onMessage", [$server, $frame, $data]);
                return;
            }
            //交给路由处理
            $status = Router::process($client->host, $uri, "upgrade", Service::WEBSOCKET);
            if ($status->status === 200 && !empty($status->controller) && class_exists($status->controller[0])) {
                //初始化处理消息的控制器
                $controller = new $status->controller[0];
                //处理中间件
                $this->processMiddleware("beforeExecute", [$controller, $status->controller[1], $data, $frame->fd]);
                if (method_exists($controller, $status->controller[1])) {
                    call_user_func_array([$controller, $status->controller[1]], [$this, $server, $data, $frame->fd]);
                    return;
                }
            }
        }
        //未知数据
        $this->processMiddleware("onUnknownMessage", func_get_args());
    }

    /**
     * 连接关闭，HTTP请示响应完成就关闭了
     * @param \Swoole\Http\Server|\Swoole\WebSocket\Server $server
     * @param int $fd
     * @param int $reactorId
     */
    public function onClose(\Swoole\Http\Server|\Swoole\WebSocket\Server $server, int $fd, int $reactorId): void
    {
        //保存server到当前协程的上下文
        Context::setServer($server);
        //执行中间件
        $this->processMiddleware("onClose", func_get_args());
        //移除客户端缓存数据
        Client::remove($server->worker_id, $fd);
    }

    /**
     * 启动服务
     */
    public function start(): void
    {
        // TODO: Implement start() method.
        $this->processMiddleware("beforeStart");
        //
        if (Service::isCLI()) {
            $this->server?->start();
        } else {
            $this->onRequest();
        }
    }

    /**
     * 重载服务
     */
    public function reload(): void
    {
        echo date("Y/m/d H:i:s")." 服务正在重启...".PHP_EOL;
        $this->server?->reload();
    }

    /**
     * 停止运行，可指定worker，默认直接关闭服务
     * @param int $workerId
     */
    public function stop(int $workerId = -2): void
    {
        if ($workerId > -2) {
            //关闭指定worker
            $this->server?->stop($workerId, true);
            echo date("Y/m/d H:i:s")." 已关闭：{$this->type}/worker {$workerId}".PHP_EOL;
        } else {
            //关闭服务，相当于 kill -15 PID
            $this->server?->shutdown();
            echo date("Y/m/d H:i:s")." 已关闭服务：{$this->type}".PHP_EOL;
        }
    }

    /**
     * 处理中间件
     * @param string $name
     * @param array $args
     */
    public function processMiddleware(string $name, array $args = []): void
    {
        //统一使用中间键处理
        Middlewares::process($name, array_merge([$this], $args));
    }

    /**
     * 热更新...
     */
    public function hotUpdate()
    {
        while (true) {
            //echo "正在执行热更新检测...".PHP_EOL;
            //检测版本
            $config = Config::get("server.hotUpdate");
            $versionFile = $config["versionFile"];
            $dirs = $config["watchDirList"] ?? [];
            $seconds = $config["seconds"] ?? 5;
            $seconds = is_numeric($seconds) && $seconds > 0 && $seconds <= 60 ? $seconds : 5;
            if (empty($dirs)) {
                //
                \Swoole\Coroutine\System::sleep($seconds);
                continue;
            }
            //开始获取文件
            $files = [];
            $suffix = $config["watchFiles"] ?? "php";
            $suffix = is_array($suffix) ? join("|", $suffix) : $suffix;
            foreach ($dirs as $dir) {
                $files = array_merge($files, File::getAllFiles($dir, $suffix));
            }
            $list = [];
            foreach ($files as $file) {
                $list[] = [
                    "md5"   => $file["md5"],
                    "file"  => $file["path"]
                ];
            }
            $json = json_encode($list, 256);
            if (is_file($versionFile)) {
                $oldMD5 = md5_file($versionFile);
                $newMD5 = md5($json);
                if ($oldMD5 !== $newMD5) {
                    if (function_exists("opcache_reset")) {
                        @opcache_reset();
                    }
                    @file_put_contents($versionFile, $json);
                    //重启
                    $this->reload();
                    //
                    echo date("Y/m/d H:i:s")." 完成热更新".PHP_EOL;
                }
            } else {
                @file_put_contents($versionFile, $json);
            }
            \Swoole\Coroutine\System::sleep($seconds);
        }
    }
}